using System.Collections;

namespace LightCAD.Three
{
    public class Vector3 : Vector, IIOArray
    {
        #region scope properties or methods
        //private static Vector3 _vector = new Vector3();
        //private static Quaternion _quaternion = new Quaternion();
        private Vector3Context getContext()
        {
            return ThreadContext.getCurrThreadContext().Vector3Ctx;
        }
        #endregion

        #region Properties

        public double x;
        public double y;
        public double z;

        #endregion

        #region constructor
        public Vector3() : this(0, 0, 0)
        { }
        public Vector3(double x = 0, double y = 0, double z = 0)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        public Vector3(Vector2 v2, double z = 0)
        {
            this.x = v2?.x ?? 0;
            this.y = v2?.y ?? 0;
            this.z = z;
        }
        #endregion

        #region methods
        public Vector3 set(double x, double y, double z = double.NaN)
        {
            if (z == double.NaN) z = this.z; // sprite.scale.set(x,y)
            this.x = x;
            this.y = y;
            this.z = z;
            return this;
        }
        public Vector3 setScalar(double scalar)
        {
            this.x = scalar;
            this.y = scalar;
            this.z = scalar;
            return this;
        }
        public Vector3 setX(double x)
        {
            this.x = x;
            return this;
        }
        public Vector3 setY(double y)
        {
            this.y = y;
            return this;
        }
        public Vector3 setZ(double z)
        {
            this.z = z;
            return this;
        }
        public Vector3 setComponent(double index, double value)
        {
            switch (index)
            {
                case 0: this.x = value; break;
                case 1: this.y = value; break;
                case 2: this.z = value; break;
                default: throw new Error("index is out of range: " + index);
            }
            return this;
        }
        public double getComponent(double index)
        {
            switch (index)
            {
                case 0: return this.x;
                case 1: return this.y;
                case 2: return this.z;
                default: throw new Error("index is out of range: " + index);
            }
        }
        public Vector3 clone()
        {
            return new Vector3(this.x, this.y, this.z);
        }
        public Vector3 copy(Vector3 v)
        {
            this.x = v.x;
            this.y = v.y;
            this.z = v.z;
            return this;
        }
        public Vector3 copy4(Vector4 v)
        {
            this.x = v.x;
            this.y = v.y;
            this.z = v.z;
            return this;
        }
        public Vector3 add(Vector3 v)
        {
            this.x += v.x;
            this.y += v.y;
            this.z += v.z;
            return this;
        }
        public Vector3 addScalar(double s)
        {
            this.x += s;
            this.y += s;
            this.z += s;
            return this;
        }
        public Vector3 addVectors(Vector3 a, Vector3 b)
        {
            this.x = a.x + b.x;
            this.y = a.y + b.y;
            this.z = a.z + b.z;
            return this;
        }
        public Vector3 addScaledVector(Vector3 v, double s)
        {
            this.x += v.x * s;
            this.y += v.y * s;
            this.z += v.z * s;
            return this;
        }
        public Vector3 sub(Vector3 v)
        {
            this.x -= v.x;
            this.y -= v.y;
            this.z -= v.z;
            return this;
        }
        public Vector3 subScalar(double s)
        {
            this.x -= s;
            this.y -= s;
            this.z -= s;
            return this;
        }
        public Vector3 subVectors(Vector3 a, Vector3 b)
        {
            this.x = a.x - b.x;
            this.y = a.y - b.y;
            this.z = a.z - b.z;
            return this;
        }
        public Vector3 multiply(Vector3 v)
        {
            this.x *= v.x;
            this.y *= v.y;
            this.z *= v.z;
            return this;
        }
        public Vector3 multiplyScalar(double scalar)
        {
            this.x *= scalar;
            this.y *= scalar;
            this.z *= scalar;
            return this;
        }
        public Vector3 multiplyVectors(Vector3 a, Vector3 b)
        {
            this.x = a.x * b.x;
            this.y = a.y * b.y;
            this.z = a.z * b.z;
            return this;
        }
        public Vector3 applyEuler(Euler euler)
        {
            return this.applyQuaternion(getContext()._quaternion.setFromEuler(euler));
        }
        public Vector3 rotateRoundAxis(Vector3 origin, Vector3 axis, double angle)
        {
            this.sub(origin); // remove the offset
            this.applyAxisAngle(axis, angle); // rotate the POSITION
            this.add(origin); // re-add the offset
            return this;
        }
        public Vector3 applyAxisAngle(Vector3 axis, double angle)
        {
            return this.applyQuaternion(getContext()._quaternion.setFromAxisAngle(axis, angle));
        }
        public Vector3 applyMatrix3(Matrix3 m)
        {
            double x = this.x, y = this.y, z = this.z;
            double[] e = m.elements;
            this.x = e[0] * x + e[3] * y + e[6] * z;
            this.y = e[1] * x + e[4] * y + e[7] * z;
            this.z = e[2] * x + e[5] * y + e[8] * z;
            return this;
        }
        public Vector3 applyNormalMatrix(Matrix3 m)
        {
            return this.applyMatrix3(m).normalize();
        }
        public Vector3 applyMatrix4(Matrix4 m)
        {
            double x = this.x, y = this.y, z = this.z;
            double[] e = m.elements;
            double w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);
            this.x = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w;
            this.y = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w;
            this.z = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w;
            return this;
        }
        public Vector3 applyQuaternion(Quaternion q)
        {
            double x = this.x, y = this.y, z = this.z;
            double qx = q.x, qy = q.y, qz = q.z, qw = q.w;
            // calculate quat * vector
            double ix = qw * x + qy * z - qz * y;
            double iy = qw * y + qz * x - qx * z;
            double iz = qw * z + qx * y - qy * x;
            double iw = -qx * x - qy * y - qz * z;
            // calculate result * inverse quat
            this.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
            this.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
            this.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
            return this;
        }
        public Vector3 project(Camera camera)
        {
            return this.applyMatrix4(camera.matrixWorldInverse).applyMatrix4(camera.projectionMatrix);
        }
        public Vector3 unproject(Camera camera)
        {
            return this.applyMatrix4(camera.projectionMatrixInverse).applyMatrix4(camera.matrixWorld);
        }
        public Vector3 transformDirection(Matrix4 m)
        {
            // input: THREE.Matrix4 affine matrix
            // vector interpreted as a direction
            double x = this.x, y = this.y, z = this.z;
            double[] e = m.elements;
            this.x = e[0] * x + e[4] * y + e[8] * z;
            this.y = e[1] * x + e[5] * y + e[9] * z;
            this.z = e[2] * x + e[6] * y + e[10] * z;
            return this.normalize();
        }
        public Vector3 divide(Vector3 v)
        {
            this.x /= v.x;
            this.y /= v.y;
            this.z /= v.z;
            return this;
        }
        public Vector3 divideScalar(double scalar)
        {
            return this.multiplyScalar(1 / scalar);
        }
        public Vector3 min(Vector3 v)
        {
            this.x = JMath.min(this.x, v.x);
            this.y = JMath.min(this.y, v.y);
            this.z = JMath.min(this.z, v.z);
            return this;
        }
        public Vector3 max(Vector3 v)
        {
            this.x = JMath.max(this.x, v.x);
            this.y = JMath.max(this.y, v.y);
            this.z = JMath.max(this.z, v.z);
            return this;
        }
        public Vector3 clamp(Vector3 min, Vector3 max)
        {
            // assumes min < max, componentwise
            this.x = JMath.max(min.x, JMath.min(max.x, this.x));
            this.y = JMath.max(min.y, JMath.min(max.y, this.y));
            this.z = JMath.max(min.z, JMath.min(max.z, this.z));
            return this;
        }
        public Vector3 clampScalar(double minVal, double maxVal)
        {
            this.x = JMath.max(minVal, JMath.min(maxVal, this.x));
            this.y = JMath.max(minVal, JMath.min(maxVal, this.y));
            this.z = JMath.max(minVal, JMath.min(maxVal, this.z));
            return this;
        }
        public Vector3 clampLength(double min, double max)
        {
            double length = this.length();
            return this.divideScalar(length == 0 ? 1 : length).multiplyScalar(JMath.max(min, JMath.min(max, length)));
        }
        public Vector3 floor()
        {
            this.x = JMath.floor(this.x);
            this.y = JMath.floor(this.y);
            this.z = JMath.floor(this.z);
            return this;
        }
        public Vector3 ceil()
        {
            this.x = JMath.ceil(this.x);
            this.y = JMath.ceil(this.y);
            this.z = JMath.ceil(this.z);
            return this;
        }
        public Vector3 round()
        {
            this.x = JMath.round(this.x);
            this.y = JMath.round(this.y);
            this.z = JMath.round(this.z);
            return this;
        }
        public Vector3 roundToZero()
        {
            this.x = (this.x < 0) ? JMath.ceil(this.x) : JMath.floor(this.x);
            this.y = (this.y < 0) ? JMath.ceil(this.y) : JMath.floor(this.y);
            this.z = (this.z < 0) ? JMath.ceil(this.z) : JMath.floor(this.z);
            return this;
        }
        public Vector3 negate()
        {
            this.x = -this.x;
            this.y = -this.y;
            this.z = -this.z;
            return this;
        }
        public double dot(Vector3 v)
        {
            return this.x * v.x + this.y * v.y + this.z * v.z;
        }
        public double lengthSq()
        {
            return this.x * this.x + this.y * this.y + this.z * this.z;
        }
        public double length()
        {
            return JMath.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
        }
        public double manhattanLength()
        {
            return JMath.abs(this.x) + JMath.abs(this.y) + JMath.abs(this.z);
        }
        public Vector3 normalize()
        {
            var len = this.length();

            return this.divideScalar(len == 0 ? 1 : len);
        }
        public Vector3 setLength(double length)
        {
            return this.normalize().multiplyScalar(length);
        }
        public Vector3 lerp(Vector3 v, double alpha)
        {
            this.x += (v.x - this.x) * alpha;
            this.y += (v.y - this.y) * alpha;
            this.z += (v.z - this.z) * alpha;
            return this;
        }
        public Vector3 lerpVectors(Vector3 v1, Vector3 v2, double alpha)
        {
            this.x = v1.x + (v2.x - v1.x) * alpha;
            this.y = v1.y + (v2.y - v1.y) * alpha;
            this.z = v1.z + (v2.z - v1.z) * alpha;
            return this;
        }
        public Vector3 cross(Vector3 v)
        {
            return this.crossVectors(this, v);
        }
        public Vector3 crossVectors(Vector3 a, Vector3 b)
        {
            double ax = a.x, ay = a.y, az = a.z;
            double bx = b.x, by = b.y, bz = b.z;
            this.x = ay * bz - az * by;
            this.y = az * bx - ax * bz;
            this.z = ax * by - ay * bx;
            return this;
        }
        public Vector3 projectOnVector(Vector3 v)
        {
            double denominator = v.lengthSq();
            if (denominator == 0) return this.set(0, 0, 0);
            double scalar = v.dot(this) / denominator;
            return this.copy(v).multiplyScalar(scalar);
        }
        public Vector3 projectOnPlane(Vector3 planeNormal)
        {
            var _vector = getContext()._vector;
            _vector.copy(this).projectOnVector(planeNormal);
            return this.sub(_vector);
        }
        public Vector3 reflect(Vector3 normal)
        {
            // reflect incident vector off plane orthogonal to normal
            // normal is assumed to have unit length
            return this.sub(getContext()._vector.copy(normal).multiplyScalar(2 * this.dot(normal)));
        }
        public double angleTo(Vector3 v)
        {
            double denominator = JMath.sqrt(this.lengthSq() * v.lengthSq());
            if (denominator == 0) return JMath.PI / 2;
            double theta = this.dot(v) / denominator;
            // clamp, to handle numerical problems
            return JMath.acos(MathUtils.clamp(theta, -1, 1));
        }
        public double distanceTo(Vector3 v)
        {
            return JMath.sqrt(this.distanceToSquared(v));
        }
        public double distanceToSquared(Vector3 v)
        {
            double dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;
            return dx * dx + dy * dy + dz * dz;
        }
        public double manhattanDistanceTo(Vector3 v)
        {
            return JMath.abs(this.x - v.x) + JMath.abs(this.y - v.y) + JMath.abs(this.z - v.z);
        }
        public Vector3 setFromSpherical(Spherical s)
        {
            return this.setFromSphericalCoords(s.radius, s.phi, s.theta);
        }
        public Vector3 setFromSphericalCoords(double radius, double phi, double theta)
        {
            double sinPhiRadius = JMath.sin(phi) * radius;
            this.x = sinPhiRadius * JMath.sin(theta);
            this.y = JMath.cos(phi) * radius;
            this.z = sinPhiRadius * JMath.cos(theta);
            return this;
        }
        public Vector3 setFromCylindrical(Cylindrical c)
        {
            return this.setFromCylindricalCoords(c.radius, c.theta, c.y);
        }
        public Vector3 setFromCylindricalCoords(double radius, double theta, double y)
        {
            this.x = radius * JMath.sin(theta);
            this.y = y;
            this.z = radius * JMath.cos(theta);
            return this;
        }
        public Vector3 setFromMatrixPosition(Matrix4 m)
        {
            double[] e = m.elements;
            this.x = e[12];
            this.y = e[13];
            this.z = e[14];
            return this;
        }
        public Vector3 setFromMatrixScale(Matrix4 m)
        {
            double sx = this.setFromMatrixColumn(m, 0).length();
            double sy = this.setFromMatrixColumn(m, 1).length();
            double sz = this.setFromMatrixColumn(m, 2).length();
            this.x = sx;
            this.y = sy;
            this.z = sz;
            return this;
        }
        public Vector3 setFromMatrixColumn(Matrix4 m, int index)
        {
            return this.fromArray(m.elements, index * 4);
        }
        public Vector3 setFromMatrix3Column(Matrix3 m, int index)
        {
            return this.fromArray(m.elements, index * 3);
        }
        public Vector3 setFromEuler(Euler e)
        {
            this.x = e._x;
            this.y = e._y;
            this.z = e._z;
            return this;
        }
        public override bool equals(Vector v)
        {
            return equals(v as Vector3);
        }
        public bool equals(Vector3 v)
        {
            return ((v.x == this.x) && (v.y == this.y) && (v.z == this.z));
        }
        public bool equals(IIOArray obj) => equals(obj as Vector3);

        public Vector3 fromArray(double[] array, int offset = 0)
        {
            this.x = array[offset];
            this.y = array[offset + 1];
            this.z = array[offset + 2];
            return this;
        }
        IIOArray IIOArray.fromArray(double[] array, int offset)
        {
            return fromArray(array, offset);
        }
        public double[] toArray(double[] array = null, int offset = 0)
        {
            if (array == null) array = new double[3];
            array[offset] = this.x;
            array[offset + 1] = this.y;
            array[offset + 2] = this.z;
            return array;
        }
        public JsArr<double> toJsArray(JsArr<double> array, int offset = 0)
        {
            if (array == null) array = new JsArr<double>(3);
            array[offset] = this.x;
            array[offset + 1] = this.y;
            array[offset + 2] = this.z;
            return array;
        }

        public Vector2 toVector2(string xname = null, string yname = null)
        {
            if (xname == null) xname = "x";
            if (yname == null) yname = "y";

            var xv = this.x;
            if (xname == "y") xv = this.y;
            else if (xname == "z") xv = this.z;

            var yv = this.y;
            if (yname == "x") yv = this.x;
            else if (xname == "z") yv = this.z;

            return new Vector2(xv, yv);
        }
        public Vector3 fromBufferAttribute(BufferAttribute attribute, int index)
        {
            this.x = attribute.getX(index);
            this.y = attribute.getY(index);
            this.z = attribute.getZ(index);
            return this;
        }
        public Vector3 random()
        {
            this.x = JMath.random();
            this.y = JMath.random();
            this.z = JMath.random();
            return this;
        }
        public Vector3 randomDirection()
        {
            // Derived from https://mathworld.wolfram.com/SpherePointPicking.html
            double u = (JMath.random() - 0.5) * 2;
            double t = JMath.random() * JMath.PI * 2;
            double f = JMath.sqrt(1 - u * 2);
            this.x = f * JMath.cos(t);
            this.y = f * JMath.sin(t);
            this.z = u;
            return this;
        }

        public Vector3 setFromColor(Color c)
        {

            this.x = c.r;
            this.y = c.g;
            this.z = c.b;

            return this;

        }
        #endregion

        public IEnumerator GetEnumerator()
        {
            yield return this.x;
            yield return this.y;
            yield return this.z;
        }

        public bool equals(object obj)
        {
            return this.equals(obj as Vector3);
        }

        public override string ToString()
        {
            return $"x: {this.x}, y: {this.y}, z: {this.z}";
        }
    }
}